使用 Rxjs 替代 Redux (三)

Author Avatar
Splendour 9月 17, 2017

前言

在前面两篇文章中,我们已经实现了用 Rxjs 替代 Redux 的整个流程,上手开发业务也没有问题了。然而,这套方法有没有什么可以优化的地方呢?

重复的代码,DRY

随着项目的增大,你会慢慢发现这些代码的出现

// userList 的 loading 状态的流
export const userListLoading$ = new BehaviorSubject(false).debug('[user]:[userListLoading]');

// userList 的数据的流
export const userList$ = new BehaviorSubject([]).debug('[user]:[userList]');

// 加载 userList 的 action
export const loadUserList = params => {
  // loading
  userListLoading$.next(true);

  request({
    method: 'GET',
    url: '/user',
    data: params,
    sucCallback: (data) => {
      // 获取到数据,让 store 执行 next
      userList$.next(data);
    },
    errCallback: (err) => {
      // 错误处理
      showErrorToast(err);
    }
  })
};

接下来,如果有其他数据的增加,对应的代码很类似,只是流和 action 的命名不同而已

DRY 是 Don’t Repeat Yourself 的缩写。意思是说,在一个设计里,对于任何东西,都应该有且只有一个表示,其它的地方都应该引用这一处。这样需要改动的时候,只需调整这一处,所有的地方就都变更过来了。

可见,我们的设计其实有点违背 DRY 原则,重复的代码过多了!

实现 entity_generator

参照之前编写 redux generator 的经验,我们来实现一个 entity_generator,使用配置的方式生成我们需要的流和 action,从而精简代码,提高开发效率

普通数据类型

export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
  • 变化的有:流的名字 data$、初始值、以及打印的前缀
  • example 属于 entity,其下包括多个数据流以及 action,data 属于其中一个数据流
  • 我们想实现的是对于每一个 entity,通过配置,生成其下的所有数据流和 action
  • 每一个配置是一个 json

具体的实现

enum type {
  DATA = 'data',
  VALUE_DATA = 'valueData',
  BOOL_DATA = 'boolData',
  ACTION = 'action',
  ASYNC_ACTION = 'asyncAction'
}

interface IConfig {
  type: type;
  name: string;

  init?: any;

  privateName?: boolean;

  handler?: (params?, entity?) => any;

  privateLoading?: boolean;
  actionLoading?: boolean;
  confirmLoading?: boolean;
  bindLoading?: boolean;
  requestOpts?: any;
  filter?: {
    init: any;
  };
  data?: {
    init: any;
    handler: (params?, response?, entity?) => any;
  }
}

type IConfigs = IConfig[];

// 暴露一个方法,传入 entityName 和配置数组,生成对应的 entity
export default (entityName: string, configs: IConfigs) => {
  // 用一个 result 来存储 entity
  const result: any = {};

  // 遍历配置,生成数据
  configs.forEach(config => {
    // 获取配置中的属性
    const {
      type,     // 类型
      name,     // 名称
      init      // 初始值
    } = config;

    // 根据 type 来生成数据
    switch (config.type) {
      case actionTypes.DATA:
        /** create data Observable */
        result[`${name}$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}]`);
        break;
    }
  });

  return result;
};

对应的配置

{
  type: actionTypes.DATA,
  name: 'data',
  init: 0
}

值类型

export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
export const setData = params => {
  data$.next(params);
};
  • 变化的有:名称、初始值、以及生成的 action 名字
  • 我们约定设置该值的 action 名字规则为 setXXX
  • 我们需要一个转换驼峰式写法的函数如下
    const toCamel = src => `${src[0].toUpperCase()}${src.substr(1, src.length)}`;
    

具体的实现

  case actionTypes.VALUE_DATA:
    /** create data Observable */
    result[`${name}$`] = new BehaviorSubject(init || 0).debug(`[${entityName}]:[${name}]`);

    /** set data operation */
    result[`set${toCamel(name)}`] = params => {
      result[`${name}$`].next(params);
    };
    break;

对应的配置

{
  type: actionTypes.VALUE_DATA,
  name: 'data',
  init: 0
}

布尔类型

export const modalShow$ = new BehaviorSubject(false).debug('[example]:[modalShow]');
export const showModal = () => {
  modalShow$.next(true);
};
export const hideModal = () => {
  modalShow$.next(false);
};
  • 变化的有:名称、初始值、以及生成的 action 名字
  • 我们约定设置该值的数据流名字为 xxxShow$,action 名字规则为 setXXX

具体的实现

  case actionTypes.BOOL_DATA:
    /** create data Observable. */
    result[`${name}Show$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}Show]`);

    /** show operation */
    result[`show${toCamel(name)}`] = () => {
      result[`${name}Show$`].next(true);
    };

    /** hide operation */
    result[`hide${toCamel(name)}`] = () => {
      result[`${name}Show$`].next(false);
    };
    break;

对应的配置

{
  type: actionTypes.BOOL_DATA,
  name: 'modal',
  init: false
}

action 类型

export const doSomething = params => data$.next('Change');
  • 变化的有:action 名字以及对应的操作
  • 我们用一个 handler(params, result) 函数来实现操作

具体的实现

case actionTypes.ACTION:
    /** create operation */
    result[`${name}`] = params => {
      handler(params, result);
    };
    break;

对应的配置

{
  type: actionTypes.ACTION
  name: 'doSomething',
  handler: (params, entity) => entity.data$.next('Change');
}

异步 action 类型

// loading、data
export const listLoading$ = new BehaviorSubject(false).debug('[example]:[listLoading]');
export const listData$ = new BehaviorSubject([]).debug('[example]:[listData]');

// 过滤器
export const listFilterData$ = new BehaviorSubject({}).debug('[example]:[listFilterData]');
export const setListFilterData = data => listFilterData$.next(data);

// 分页
export const listCurPage$ = new BehaviorSubject(1).debug('[example]:[listCurPage]');
export const setListCurPage = data => listCurPage$.next(data);
export const listPageSize$ = new BehaviorSubject(20).debug('[example]:[listPageSize]');
export const setListPageSize = data => listPageSize$.next(data);
export const listTotal$ = new BehaviorSubject(0).debug('[example]:[listTotal]');

// 请求
export const list = params => {

  listLoading$.next(true);

  request({
    method: 'GET',
    url: '/list',
    data: params,
    sucCallback: (data) => {
      listLoading$.next(false);
      listData$.next(data.list);
      listTotal$.next(data.count);
    },
    errCallback: (err) => {
      listLoading$.next(false);
      showErrorToast(err);
    }
  })
};

只要做一下约定,就可以让以上数据标准化。

具体的实现

case actionTypes.ASYNC_ACTION:

    /** private loading,auto emit data in async operation */
    if (privateLoading) {
      result[`${name}Loading$`] = new BehaviorSubject(false).debug(`[${entityName}]:[${name}Loading]`);
    }

    /** create data Observable,auto emit data when request success */
    if (data) {
      result[`${name}Data$`] = new BehaviorSubject(data.init).debug(`[${entityName}]:[${name}Data]`);
    }

    /** create filter Observable */
    if (filter) {
      result[`${name}FilterData$`] = new BehaviorSubject(filter.init).debug(`[${entityName}]:[${name}FilterData]`);
      result[`set${toCamel(name)}FilterData`] = data => {
        result[`${name}FilterData$`].next(data);
      };
    }

    if (pagination) {
      result[`${name}CurPage$`] = new BehaviorSubject(1).debug(`[${entityName}]:[${name}CurPage]`);
      result[`set${toCamel(name)}CurPage`] = data => {
        result[`${name}CurPage$`].next(data);
      };

      result[`${name}PageSize$`] =
        new BehaviorSubject(pagination.pageSize || 20).debug(`[${entityName}]:[${name}PageSize]`);
      result[`set${toCamel(name)}PageSize`] = data => {
        result[`${name}PageSize$`].next(data);
      };

      result[`${name}Total$`] = new BehaviorSubject(0).debug(`[${entityName}]:[${name}Total]`);
    }

    /** create the main operation */
    result[name] = (params?) => {
      /** loading => true */
      privateLoading && result[`${name}Loading$`].next(true);

      request({
        method: requestOpts.method,
        url: requestOpts.url(params, result),

        /** requestOpts.data is a function that return data according to params and the entity */
        data: requestOpts.data && requestOpts.data(omit(['sucCallback', 'errCallback'], params), result),

        sucCallback: response => {
          /** loading => false */
          privateLoading && result[`${name}Loading$`].next(false);

          /** set data according to response */
          data && result[`${name}Data$`].next(data.handler(params, response, result));

          /** set total according to response if pagination */
          pagination && result[`${name}Total$`].next(pagination.total(params, response, result));

          /** do callback if needed */
          requestOpts.sucCallback && requestOpts.sucCallback(params, response, result);
        },
        errCallback: err => {
          /** loading => false */
          privateLoading && result[`${name}Loading$`].next(false);

          /** do callback if needed */
          requestOpts.errCallback && requestOpts.errCallback(params, err, result);
        }
      });
    };

    break;

对应的配置

  {
    type: actionTypes.ASYNC_ACTION,
    name: 'list',
    privateLoading: true,
    requestOpts: {
      method: 'GET',
      url: (params, entity) => `/list`,
      data: (params, entity) => Object.assign({},
        entity[`listFilterData$`].getValue(), {
          limit: entity[`listPageSize$`].getValue(),
          offset: (entity[`listCurPage$`].getValue() - 1) * entity[`listPageSize$`].getValue()
        }, params)
    },
    filter: {
      init: {}
    },
    pagination: {
      total: (params, response, entity) => response.count
    },
    data: {
      init: [],
      handler: (params, response, entity) => response.list
    }
  }

总结

通过实现以上的 entity_generator,在开发过程中不仅节省了代码,也提高了开发效率,约定了标准的命名规范,最终提升了协同的效率。

记住,Don’t repeat yourselef.